#include "stdafx.h"
#include "execute.h"
#include "includeFTLib.h"
#include "utils.h"
#include "AveragingPerformanceData.h"

using namespace FTLib;
using namespace std;

static FTLib::FourierTransformEngines Convert(FFTEngine eng)
{
	switch (eng)
	{
	case FFTEngine::CuFFT:
		return FourierTransformEngines::CuFft;
	case FFTEngine::IppFFT:
		return FourierTransformEngines::Ipp;
	case FFTEngine::FFTWFFT:
		return FourierTransformEngines::FFTW;
	case FFTEngine::D3DCSX:
		return FourierTransformEngines::D3DCSX;
	case FFTEngine::CLFFT:
		return FourierTransformEngines::CLFFT;
	default:
		throw std::invalid_argument("Unknown FFTEngine.");
	}
}

static std::shared_ptr<IFourierTransformFactory> CreateFftEngine(const CCmdLineOptions& options)
{
	return CreateFourierTransformEngine(Convert(options.GetFFTEngine()));
}

static std::wstring GetFftEngineInfo(const CCmdLineOptions& options, FTLib::EngineInfoType infoType)
{
	std::wstring str;
	FTLib::GetEngineInfo(infoType, Convert(options.GetFFTEngine()), str);
	return str;
}

template <class tEngine>
void DoTransformationAndBenchmark(const tEngine& eng, std::shared_ptr<IBitmapData> src, std::shared_ptr<IBitmapData>& output, const CCmdLineOptions& options, std::shared_ptr<ILog> log)
{
	if (!options.DoBenchmark())
	{
		TransformNewOutput(eng, src, output, nullptr);
	}
	else
	{
		TransformNewOutput(eng, src, output, nullptr);

		CAveragingPerformanceDataStore averagingPerfData;
		FourierTransformAdditional additional = { &averagingPerfData };

		auto result = BenchmarkFunction(
			[&]() -> void
		{
			Transform(eng, src, output, &additional);
		},
			(std::max)(options.GetBenchmarkRepeatCount(), 1u));

		log->WriteStdOutLine(L"time for %u calls: %lfsec ; one call took %lfsec", result.GetRepeats(), result.TotalTimeInSeconds(), result.GetTimePerCall());
		if (!averagingPerfData.isEmpty())
		{
			averagingPerfData.EnumeratePerformanceData(
				[=](const std::wstring& key, double sum, int count)->bool
			{
				log->WriteStdOutLine(L"  \"%ls\" -> %lfms", key.c_str(), sum / count);
				return true;
			});
		}
	}
}

static std::shared_ptr<IBitmapData> DoTransformation(std::shared_ptr<IFourierTransformFactory> factory, std::shared_ptr<IBitmapData> src, const CCmdLineOptions& options, std::shared_ptr<ILog> log)
{
	IFourierTransformFactory::Options opts;
	opts.width = src->GetWidth();
	opts.height = src->GetHeight();
	opts.normalizationFactor = options.GetNormalizationFactor();

	std::shared_ptr<IBitmapData> output;

	if (options.GetTransformationType() == TransformationType::R2C)
	{
		switch (options.GetFourierTransformDataLayout())
		{
		case FourierTransformedDataLayout::Complex:
		{
			auto eng = factory->CreateRealToComplex(opts);
			DoTransformationAndBenchmark(eng, src, output, options, log);
		}

		break;
		case FourierTransformedDataLayout::PackedComplex_FftwFormat:
		{
			auto eng = factory->CreateRealToComplexPackedFftw(opts);
			DoTransformationAndBenchmark(eng, src, output, options, log);
		}

		break;
		case FourierTransformedDataLayout::PackedComplex_IppFormat:
		{
			auto eng = factory->CreateRealToComplexPackedIpp(opts);
			DoTransformationAndBenchmark(eng, src, output, options, log);
		}

		break;
		default:
			break;
		}
	}
	else if (options.GetTransformationType() == TransformationType::C2C_FWD)
	{
		switch (options.GetFourierTransformDataLayout())
		{
		case FourierTransformedDataLayout::Complex:
		{
			auto eng = factory->CreateComplexToComplexFwd(opts);
			DoTransformationAndBenchmark(eng, src, output, options, log);
		}

		break;
		}
	}
	else if (options.GetTransformationType() == TransformationType::C2C_INV)
	{
		switch (options.GetFourierTransformDataLayout())
		{
		case FourierTransformedDataLayout::Complex:
		{
			auto eng = factory->CreateComplexToComplexInv(opts);
			DoTransformationAndBenchmark(eng, src, output, options, log);
		}

		break;
		}
	}
	else if (options.GetTransformationType() == TransformationType::C2R)
	{
		switch (options.GetFourierTransformDataLayout())
		{
		case FourierTransformedDataLayout::Complex:
		{
			auto eng = factory->CreateComplexHermitianToReal(opts);
			DoTransformationAndBenchmark(eng, src, output, options, log);
		}

		break;
		}
	}

	return output;
}

static OutputFileNameType DetermineOutputFilenameType(SaveDataFormat saveDataFmt, IBitmapData* data)
{
	if (saveDataFmt != SaveDataFormat::CSV && saveDataFmt != SaveDataFormat::BINARY)
	{
		return OutputFileNameType::CSV_Complex;
	}

	switch (data->GetPixelType())
	{
	case PixelType::GrayFloat32:
	case PixelType::GrayDouble64:
	case PixelType::Gray16:
	case PixelType::Gray8:
		return (saveDataFmt == SaveDataFormat::CSV) ? OutputFileNameType::CSV_Real : OutputFileNameType::BINARY_Real;
	case PixelType::ComplexDouble64:
	case PixelType::ComplexFloat32:
		return (saveDataFmt == SaveDataFormat::CSV) ? OutputFileNameType::CSV_Complex : OutputFileNameType::BINARY_Complex;
	}

	throw std::logic_error("cannot determine output-filename-type");
}

static void SaveResult(std::shared_ptr<IBitmapData> result, const CCmdLineOptions& options)
{
	std::wstring defExtension, destFilename;
	switch (options.GetOutputFileFormat())
	{
	case OutputFormat::CSV:
	{
		options.GetDefaultFileExtension(OutputFormat::CSV, defExtension);
		destFilename = options.GenerateOutputFilename(DetermineOutputFilenameType(SaveDataFormat::CSV, result.get())/*OutputFileNameType::CSV_Complex*/, defExtension.c_str());
		SaveData(destFilename.c_str(), result, SaveDataFormat::CSV);
	}

	break;
	case OutputFormat::BINARY:
	{
		options.GetDefaultFileExtension(OutputFormat::BINARY, defExtension);
		destFilename = options.GenerateOutputFilename(DetermineOutputFilenameType(SaveDataFormat::BINARY, result.get())/*OutputFileNameType::BINARY_Complex*/, defExtension.c_str());
		SaveData(destFilename.c_str(), result, SaveDataFormat::BINARY);
	}

	break;
	}
}

static const wchar_t* pixelTypeToString(PixelType pixelType)
{
	switch (pixelType)
	{
	case PixelType::GrayFloat32:
		return L"GrayFloat32";
	case 	PixelType::GrayDouble64:
		return L"GrayDouble64";
	case 	PixelType::ComplexFloat32:
		return L"ComplexFloat32";
	case 	PixelType::ComplexDouble64:
		return L"ComplexDouble64";
	case 	PixelType::Gray16:
		return L"Gray16";
	case 	PixelType::Gray8:
		return L"Gray8";
	}

	return L"unknown";
}

static void PrintSourceInfo(std::shared_ptr<IBitmapData> srcData, std::shared_ptr<ILog> log)
{
	std::wostringstream out;
	out << L"Source: " << srcData->GetWidth() << L" x " << srcData->GetHeight() << L" pixeltype=" << pixelTypeToString(srcData->GetPixelType()) << "\n";
	log->WriteInfoString(1, out.str());
}

static std::shared_ptr<IBitmapData> GetSrcData(const CCmdLineOptions& options, std::shared_ptr<ILog> log)
{
	uint32_t width, height; float randMin, randMax; uint32_t randSeed;
	if (options.GetRandomSrcParameters(&width, &height, &randMin, &randMax, &randSeed) == false)
	{
		try
		{
			return ReadDataAsFloat32(options.GetSourceFilename().c_str());
		}
		catch (const FTLib::loadsavedata_exception& e)
		{
			log->WriteStdOut(e.getErrorMessage().c_str());
			return std::shared_ptr<IBitmapData>();
		}
	}
	else
	{
		switch (options.GetTransformationType())
		{
		case TransformationType::R2C:
			return CreateRandomFloat32(width, height, PixelType::GrayFloat32, randMin, randMax, randSeed);
		case TransformationType::C2C_FWD:
		case TransformationType::C2C_INV:
			return CreateRandomFloat32(width, height, PixelType::ComplexFloat32, randMin, randMax, randSeed);
		case TransformationType::C2R:
			// TODO....
			return CreateRandomFloat32(width, height, PixelType::Gray8, randMin, randMax, randSeed);
		}

		throw std::invalid_argument("unknwon transformation-type.");
	}
}

static bool ExecuteTransform(const CCmdLineOptions& options, std::shared_ptr<ILog> log)
{

	std::shared_ptr<IBitmapData> srcData = GetSrcData(options, log);
	if (!srcData)
	{
		return false;
	}

	/*try
	{
	srcData = ReadDataAsFloat32(options.GetSourceFilename().c_str());
	}
	catch (const FTLib::loadsavedata_exception& e)
	{
	log->WriteStdOut(e.getErrorMessage().c_str());
	return false;
	}*/

	if (log->IsInfoEnabled(1))
	{
		PrintSourceInfo(srcData, log);
	}

	auto ftEng = CreateFftEngine(options);
	if (!ftEng)
	{
		log->WriteStdOut(L"Couldn't create engine \"%ls\".", GetFftEngineInfo(options, EngineInfoType::Name).c_str());
		return false;
	}

	auto result = DoTransformation(ftEng, srcData, options, log);

	SaveResult(result, options);
	return true;
}

static bool ExecuteListVersions(const CCmdLineOptions& options, std::shared_ptr<ILog> log)
{
	auto engines = FTLib::GetListOfEngines();
	for each (auto engType in engines)
	{
		std::wstring name, version;
		FTLib::GetEngineInfo(FTLib::EngineInfoType::Name, engType, name);
		bool available = IsEngineAvailable(engType);
		if (available)
		{
			FTLib::GetEngineInfo(FTLib::EngineInfoType::Version, engType, version);
		}

		log->WriteStdOutLine(L"Engine: %s   operational=%hs", name.c_str(), available ? "true" : "false");
		if (available == true)
		{
			log->WriteStdOutLine(L"-> %s", version.c_str());
		}

		log->WriteStdOutLine(L"");
	}

	return true;
}

static bool ExecutePassThroughSource(const CCmdLineOptions& options, std::shared_ptr<ILog> log)
{
	std::shared_ptr<IBitmapData> srcData = GetSrcData(options, log);
	if (!srcData)
	{
		return false;
	}

	if (log->IsInfoEnabled(1))
	{
		PrintSourceInfo(srcData, log);
	}

	SaveResult(srcData, options);
	return true;
}

bool Execute(const CCmdLineOptions& options, std::shared_ptr<ILog> log)
{
	auto additionalDirs = options.GetAdditionalDllSearchPath();
	if (additionalDirs.size() > 0)
	{
		auto it = additionalDirs.cbegin();
		auto end = additionalDirs.cend();
		FTLib::InitializeFTLib(
			[=, &it]() -> const wchar_t*
		{
			return (it != end) ? it++->c_str() : nullptr;
		});
	}

	bool b = false;
	if (options.GetCommand() == Command::Transform)
	{
		b = ExecuteTransform(options, log);
	}
	else if (options.GetCommand() == Command::ListVersions)
	{
		b = ExecuteListVersions(options, log);
	}
	else if (options.GetCommand() == Command::PassThroughSource)
	{
		b = ExecutePassThroughSource(options, log);
	}

	FTLib::ShutdownFTLib();

	return b;
}